#include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <byteswap.h> // gnu c lib
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#include <cmath>


const auto DEBUG_IMAGE = 0;
const auto FULLSCREEN_W = 0;
const auto FULLSCREEN_H = 0;

const auto ZOOM_TIME = 0.3f;
const auto GRID_DEFAULT_ALPHA = 30;

static float g_zoom_time = ZOOM_TIME;
static int g_grid_alpha = GRID_DEFAULT_ALPHA;

void exit_with_msg(const char* msg)
{
    printf("[ERROR]: %s\n", msg);
    exit(1);
}

struct Zoomer {
    SDL_Window* window;
    XWindowAttributes root_attributes;
    uint32_t* image;
    struct SDL_Renderer* renderer;
};

#define STRIT(x) #x // "string" (x) convert

static SDL_Window*
init_window(const char* window_name, int width, int height)
{
    SDL_Window* window;
    Uint32 flag;

    flag = SDL_WINDOW_HIDDEN;// hide window flag
    if (width == FULLSCREEN_W &&
        height == FULLSCREEN_H
    ) flag |= SDL_WINDOW_FULLSCREEN_DESKTOP;

    window = SDL_CreateWindow(window_name, SDL_WINDOWPOS_UNDEFINED, 
        SDL_WINDOWPOS_UNDEFINED, width, height, flag
    );

    if (NULL == window)
        exit_with_msg("window failed to init");

    return window;
}

static SDL_Renderer*
init_renderer(SDL_Window* window)
{
    SDL_Renderer* renderer;

    renderer = SDL_CreateRenderer(window, -1, 0);
    if (NULL == renderer)
        exit_with_msg("render failed to init");

    return renderer;
}

static uint32_t
rgb_to_abgr(uint32_t rgb, uint8_t alpha)
{
    return (alpha << 24) + __bswap_32(rgb << 8);
}

static void
install_zoomer(Zoomer* zoomer, const char* name, int width, int height)
{
    SDL_SysWMinfo info;
    Display* display;//x11 stuff
    Window root;
    XImage* image;
    int row;
    int col;
    unsigned long i;

    SDL_SetHint("SDL_VIDEODRIVER", "x11");// set deriver to x11, use x11 for backend

    if (SDL_Init(SDL_INIT_VIDEO) < 0)
        exit_with_msg("sdl init video with driver x11 failed");

    atexit(SDL_Quit);// set a callback func

    zoomer->window = init_window(name, width, height);

    SDL_VERSION(&info.version);// sys info version 

    if (!SDL_GetWindowWMInfo(zoomer->window, &info))
        exit_with_msg("sdl get window wm info failed");

    display = info.info.x11.display;

    root = XDefaultRootWindow(display);

    if (XGetWindowAttributes(display, root, &zoomer->root_attributes) == 0)
        exit_with_msg("x11 get window attribute failed");

    image = XGetImage(display, root, 0, 0,
        zoomer->root_attributes.width,
        zoomer->root_attributes.height,
        AllPlanes, ZPixmap
    );

    if (NULL == image)
        exit_with_msg("image is null");

    i = 0;
    zoomer->image = (uint32_t*)malloc(
        sizeof(*zoomer->image) * (image->width * image->height)
    );

    for (row = 0; row < image->height; ++row)
    {
        for (col = 0; col < image->width; ++col)
        {
            unsigned long pixel;
            pixel = XGetPixel(image, col, row);
            zoomer->image[i++] = rgb_to_abgr(pixel, 0xff);
        }
    }

    XFree(image);

    SDL_ShowWindow(zoomer->window);

    zoomer->renderer = init_renderer(zoomer->window);
}

static void
destroy_zoomer(Zoomer* zoomer)
{
    SDL_DestroyWindow(zoomer->window);
    SDL_DestroyRenderer(zoomer->renderer);
}

static float
nearest_multiple(float x, float m)
{
    return m * floorf(x / m);
}

static void
snap_to_grid(const SDL_Rect* stat, const SDL_Rect* dyn,
             int mx, int my, float* x, float* y, float* w, float* h)
{
    *w = (float)stat->w / (float)dyn->w;
    *h = (float)stat->h / (float)dyn->h;

    *x = nearest_multiple((float)mx, *w);
    *y = nearest_multiple((float)my, *h);
}

#define APP_NAME "sdlmag"
void usage()
{
    printf("USAGE: %s <options>\n"
                 "-h -- Display this app helper doc\n"
                 "-t <float> -- Specifies zoom time\n",
            APP_NAME            
    );

    exit(0);
}

#define this_min(a, b) ((a) < (b) ? (a) : (b))
#define this_max(a, b) ((a) > (b) ? (a) : (b))

#define clamped(a, b, c)    this_max( (b) , this_min( (a), (c) ))
#define lerp(a, b, t)       (a) + (t) * ( (b) - (a) )

int main(int argc, char** argv)
{

    Zoomer zoomer;
    SDL_Texture* buffer;
    int pitch;
    SDL_Rect stat;
    SDL_Rect dyn;

    int disps;
    int gzw; int gzh;
    int zw;  int zh;
    int px;  int py;
    int w;   int h;
    
    Uint64 now; Uint64 last;
    float dt; float zt;
    float ezt; float p;
    int i; bool grid;
    bool altmod;

    struct {
        float y;
        float p;
    } hold;

    for (i = 1; i < argc; i++)
    {
        const char* arg = argv[i];

        if (*arg != '-') usage();

        switch (arg[1]) {
        case 'h': usage(); break;
        case 't': {
            if (argc < (i + 1))
            {
                usage();
            }

            i += 1;
            g_zoom_time = atof(argv[i]);
        }break;

        default: {
            printf("unknown input: %s\n\n", &arg[1]);
            usage();
        }break;
        }
    }

    install_zoomer(&zoomer, "zoomer", FULLSCREEN_W, FULLSCREEN_H);

    buffer = SDL_CreateTexture(zoomer.renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING,
                               zoomer.root_attributes.width, zoomer.root_attributes.height
    );

    if (NULL == buffer) 
        exit_with_msg("SDL create texture, output buffer is null");

    SDL_GetWindowSize(zoomer.window, &w, &h);

    if ( (disps = SDL_GetNumVideoDisplays()) < 0 )
        exit_with_msg("SDL get numbers of video displays is less than 0, maybe no screen display on your pc");

    stat = (SDL_Rect){.x = 0, .y = 0, .w = w, .h = h};

    px = -1; py = -1;

    gzw = 0; gzh = 0;

    zw = 0; zh = 0;
    zt = 0.f;
    ezt = 0.f;

    grid = false;
    altmod = false;

    hold.y = -1.f; hold.p = -1.f;

    now = SDL_GetPerformanceCounter();
    last = 0;
    dt = 0.f;

    for (; ;)
    {
        SDL_Event event;
        int mx; int my;
        Uint32 b;
        float dw; float dh;
        SDL_FRect rect;

        if (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT || 
                (event.type == SDL_WINDOWEVENT && 
                 event.window.event == SDL_WINDOWEVENT_CLOSE
                )
            ) break;

            if (event.type == SDL_KEYUP)
            {
                switch (event.key.keysym.sym){
                case SDLK_g: { // typing 'g'
                    grid = !grid;
                }break;
                case SDLK_LALT: { // typing left 'alt'
                    altmod = false;
                }break;
                }
            } else if (event.type == SDL_KEYDOWN)
            {
                switch (event.key.keysym.sym) {
                case SDLK_LALT: {
                    altmod = true;
                }break;
                }

                if (event.type == SDL_KEYDOWN &&
                    event.key.keysym.sym > SDLK_0 &&
                    event.key.keysym.sym <= SDLK_9
                ) {
                    int dispidx;

                    dispidx = event.key.keysym.sym - SDLK_0;

                    if (dispidx <= disps) {
                        if (SDL_GetDisplayBounds(dispidx - 1, &stat) != 0)
                            exit_with_msg("SDL get display bound failed");

                        stat.w = w; stat.h = h;
                        zw = gzw = zh = gzh = 0;
                    }
                }
            }

            if (event.type == SDL_MOUSEWHEEL) // mouse slide 
            {
                if (!altmod)
                {
                    gzw = clamped(gzw + event.wheel.y * (stat.w * 0.03), 0,
                                  stat.w / 2 - 3
                    );

                    gzh = clamped(gzh + event.wheel.y * (stat.h * 0.03), 0,
                                  stat.h / 2 - 3
                    );

                    zt = dt;
                    ezt = zt + g_zoom_time;
                } else {
                    g_grid_alpha = clamped(g_grid_alpha + event.wheel.y * 10, 10, 120);
                }
            }

        } // if poll event

        p = std::fmaxf(0, (ezt - dt) / g_zoom_time);
        zw = lerp(zw, gzw, std::sinf(1.f - p)); zh = lerp(zh, gzh, std::sinf(1.f - p));

        if (hold.p != p)
            hold.y = -1.f;

        hold.p = p;

        b = SDL_GetMouseState(&mx, &my);

        if (SDL_BUTTON(b) == (1 << SDL_BUTTON_RIGHT) && 
            (px != -1) && (py != -1)
        ) {
            stat.x -= (mx - px); 
            stat.y -= (my - py);

            hold.y = -1.f;
        }

        stat.x = clamped(stat.x, -zw, (zoomer.root_attributes.width - stat.w) + zw - 1);
        stat.y = clamped(stat.y, -zh, (zoomer.root_attributes.height - stat.h) + zh - 1);

        dyn = stat;
        dyn.x += zw; dyn.y += zh;
        dyn.w -= zw * 2; dyn.h -= zh * 2;

        SDL_RenderCopy(zoomer.renderer, buffer, &dyn, NULL);

        if (grid)
        {
            dw = (float)stat.w / (float)dyn.w;
            dh = (float)stat.h / (float)dyn.h;

            SDL_SetRenderDrawBlendMode(zoomer.renderer, SDL_BLENDMODE_BLEND);
            SDL_SetRenderDrawColor(zoomer.renderer, 255, 255, 255, g_grid_alpha);

            for (i = 1; i < (stat.w / (int)dh); i++) {
                SDL_RenderDrawLineF(zoomer.renderer, 0.f,
                                    ((float)i * dh),
                                    (float)stat.w,
                                    ((float)i * dh)
                );
            }
        }

        SDL_RenderPresent(zoomer.renderer);

        if (event.key.keysym.sym == SDLK_LSHIFT)
        {
            if (event.type == SDL_KEYDOWN && 
                hold.y == -1.f
            ) {
                hold.y = rect.y;
            } else if (event.type == SDL_KEYUP)
            {
                hold.y = -1.f;
            }
        }


        last = now;
        now = SDL_GetPerformanceCounter();
        dt += (float)((now - last) / (float)SDL_GetPerformanceFrequency());

        px = mx;
        py = my;
    }


    SDL_DestroyTexture(buffer);

    destroy_zoomer(&zoomer);

    return 0;
}